const path = require("path").posix;
const fs = require('fs');
// 生成特定的语法树节点
const types = require('@babel/types');
// 转化
const parser = require('@babel/parser');
// 遍历
const traverse = require('@babel/traverse').default;
// 生成
const generator = require('@babel/generator').default;
const baseDir = toUnixPath(process.cwd());
function toUnixPath(filePath) {
    return filePath.replace(/\\/g, "/");
}
class Complation {
    constructor(options) {
        this.options = options;
        // 本次编译所有的模块
        this.modules = [];
        // 所有的代码块
        this.chunks = [];
        // 产物资源
        this.assets = {};
        // 当前编译依赖的文件
        this.fileDependences = [];
    }
    build(onCompiled) {
        // 5.根据配置中的 entry 找出入口文件
        // entry 的两种写法：字符串和对象
        let entry = {};
        if (typeof this.options.entry === "string") {
            entry.main = this.options.entry;
        } else {
            entry = this.options.entry;
        }
        for (let entryName in entry) {
            // F:/Web/web-project/my-webpack/src/entry1.js
            let entryPath = path.join(baseDir, entry[entryName]);
            this.fileDependences.push(entryPath);
            // 6.从入口文件出发，调用所有配置的 loader 对模块进行编译
            let entryModule = this.buildModule(entryName, entryPath);
            // console.log('entryModule:', entryModule)
            // 8.根据入口和模块之间的依赖关系，组装成一个个包含多个模块的Chunk
            let chunk = {
                // 代码块名标是入口名称
                name: entryName,
                // 入口模块
                entryModule,
                // 代码块中包含哪些模块
                modules: this.modules.filter(module => module.names.includes(entryName))
            }
            this.chunks.push(chunk);
            // 9.再把每个 Chunk 转换成一个单独的文件加入到输出列表
            this.chunks.forEach(chunk => {
                let filename = this.options.output.filename.replace('[name]', chunk.name);
                this.assets[filename] = getSource(chunk);
            })
        }
        onCompiled(null, {
            modules: this.modules,
            chunks: this.chunks,
            assets: this.assets
        }, this.fileDependences);
    }
    /**
     * 编译模块
     * @param {*} moduleName 入口名称 entry1 entry2
     * @param {*} modulePath 模块路径 F:/Web/web-project/my-webpack/src/entry1.js
     * @returns 
     */
    buildModule(moduleName, modulePath) {
        // 6.从入口文件出发，调用所有配置的 Loader 对模块进行编译
        // 6.1 读取源代码的内容
        let sourceCode = fs.readFileSync(modulePath, 'utf8');
        // 6.2 匹配此模块需要使用的 loader
        let { rules } = this.options.module;
        let loaders = [];
        rules.forEach(rule => {
            // 如果 rule 中的 test 和文件后缀匹配上了 则把此 rule 对应的 loader 添加到 loaders 中
            if (modulePath.match(rule.test)) {
                loaders.push(...rule.use);
            }
        });
        sourceCode = loaders.reduceRight((sourceCode, loader) => {
            return require(loader)(sourceCode);
        }, sourceCode)
        // 7.再找出该模块依赖的模块，再递归本步骤(buildModule)直到所有入口依赖的文件都经过了本步骤的处理
        // 获取模块id moduleId：相对于项目根目录的相对路径 "./src/entry1.js"
        let moduleId = './' + path.relative(baseDir, modulePath);
        // console.log('moduleId:', moduleId)
        // 创建模块对象
        // moduleId：相对于项目根目录的相对路径
        // dependences 表示此模块依赖的模块
        // names 表示此模块添几个入口依赖了，入口的名称 ['entry1', 'entry2'] 即该模块被几个入口引用了
        let module = { id: moduleId, dependences: [], names: [moduleName] }
        // 将源码转化为抽象语法树
        // module 可兼容ES语法和CommonJS语法
        const ast = parser.parse(sourceCode, { sourceType: 'module' });
        // 7.1 再找出该模块依赖的模块，
        traverse(ast, {
            CallExpression: ({ node }) => {
                if (node.callee.type === "Identifier" &&
                    node.callee.name === "require" &&
                    node.arguments.length > 0 &&
                    node.arguments[0].type === "StringLiteral"
                ) {
                    // 依赖的路径: "./title" 
                    let depModuleName = node.arguments[0].value;
                    // 获取当前模块所在的的目录：F:/Web/web-project/my-webpack/src
                    let dirname = path.dirname(modulePath);
                    // 获取依赖模块的绝对路径：F:/Web/web-project/my-webpack/src/title 缺少扩展名
                    let depModulePath = path.join(dirname, depModuleName);
                    // 获取当前支持的扩展名
                    let extensions = this.options.resolve.extensions;
                    // F:/Web/web-project/my-webpack/src/title.js
                    depModulePath = tryExtension(depModulePath, extensions);
                    // 把此依赖文件添加到依赖数组里，当文件变化了，会重新启动编译，创建一个新的Compilation
                    this.fileDependences.push(depModulePath);
                    // 获取依赖模块的模块id：相对于项目根目录的相对路径
                    let depModuleId = './' + path.relative(baseDir, depModulePath);
                    // 修改AST语法树 把 require 方法的参数变成依赖的模块的id
                    // [ { type: 'StringLiteral', value: './src/title.js' } ]
                    node.arguments = [types.stringLiteral(depModuleId)];
                    // 把依赖信息添加到依赖数组
                    module.dependences.push({ depModuleId, depModulePath })
                }
            }
        });
        let { code } = generator(ast);
        module._source = code;
        // 7.2 再递归本步骤(buildModule)直到所有入口依赖的文件都经过了本步骤的处理
        module.dependences.forEach(({ depModuleId, depModulePath }) => {
            let buildModule = this.modules.find(module => module.id === depModuleId);
            if (buildModule) {
                // title 这个模块同时被 entry1 和 entry2 引用，不需要重复编译
                // module.name = ['entry1', 'entry2']
                buildModule.names.push(moduleName);
            }
            else {
                let depModule = this.buildModule(moduleName, depModulePath);
                this.modules.push(depModule);
            }
        })
        return module;
    }
}
/**
 * 尝试给当前模块添加扩展名 知道找到存在的文件为止
 * @param {*} modulePath 
 * @param {*} extensions 
 */
function tryExtension(modulePath, extensions) {
    if (fs.existsSync(modulePath)) {
        return modulePath;
    }
    for (let i = 0; i < extensions.length; i++) {
        let filePath = modulePath + extensions[i];
        if (fs.existsSync(filePath)) {
            return filePath;
        }
    }
    throw new Error(`找不到${modulePath}`);
}
function getSource(chunk) {
    return `
    (() => {
        var __webpack_modules__ = {
            ${chunk.modules.map(
            (module) => `
                "${module.id}": (module, exports, require) => {
                    ${module._source}
                },
                `
            )}
        };
        var __webpack_module_cache__ = {};
        function require(moduleId) {
            var cachedModule = __webpack_module_cache__[moduleId];
            if (cachedModule !== undefined) {
                return cachedModule.exports;
            }
            var module = __webpack_module_cache__[moduleId] = {
                exports: {}
            };
            __webpack_modules__[moduleId](module, module.exports, require);
            return module.exports;
        }
        var __webpack_exports__ = {};
        (() => {
            ${chunk.entryModule._source}
        })();
    })();
    `
}
module.exports = Complation;
